package cn.imatu.framework.flyway;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.imatu.framework.flyway.entity.Flyway;
import cn.imatu.framework.flyway.entity.FlywaySql;
import cn.imatu.framework.flyway.entity.ResourceScript;
import cn.imatu.framework.flyway.executor.FlywayExecutor;
import cn.imatu.framework.flyway.mapper.FlywayMapper;
import cn.imatu.framework.flyway.properties.LyFlywayProperties;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author shenguangyang
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class FlywayManager {
    private final LyFlywayProperties lyFlywayProperties;
    private final FlywayMapper flywayMapper;

    /**
     * 删除注释
     */
    private String removeComment(String sql) {
        if (sql.trim().toUpperCase(Locale.ROOT).startsWith("INSERT ")) {
            return sql;
        }
        // 匹配和过滤 SQL 语句中的注释
        Pattern p = Pattern.compile("(?ms)('(?:''|[^'])*')|--.*?$|//.*?$|/\\*.*?\\*/|#.*?$|");
        return p.matcher(sql).replaceAll("$1");
    }

    private String readFile(InputStream in) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        String content;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
            String line;
            while((line = reader.readLine()) != null) {
                if (line.contains("--") || StrUtil.isEmpty(line.replaceAll(" ", ""))) {
                    continue;
                }
                stringBuilder.append(line).append("\n");
            }
            content = this.removeComment(stringBuilder.toString());
        }
        return content;
    }

    private List<Flyway> getDBFlyway(String type) {
        LambdaQueryWrapper<Flyway> query = Wrappers.lambdaQuery();
        query.eq(Flyway::getType, type).orderByAsc(Flyway::getInstalledRank);
        return this.flywayMapper.selectList(query);
    }

    public void initConfig(FlywayExecutor flywayExecutor, ResourceScript r) {
        List<Flyway> dbFlyways = this.getDBFlyway(r.getType());
        this.checkVersion(dbFlyways, r.getSqlList());
        List<FlywaySql> sqlList = this.listExecuteSql(dbFlyways, r.getSqlList());
        if (CollUtil.isEmpty(sqlList)) {
            return;
        }
        sqlList.forEach(flywayExecutor::execSql);
    }

    private List<FlywaySql> listExecuteSql(List<Flyway> dbFlyways, List<FlywaySql> resourceFlyways) {
        List<FlywaySql> result = new ArrayList<>();
        resourceFlyways.forEach(flywaySql -> {
            if (!ObjectUtils.isEmpty(dbFlyways)) {
                boolean flg = true;
                for (Flyway dbFlyway : dbFlyways) {
                    if (Objects.equals(dbFlyway.getInstalledRank(), flywaySql.getInstalledRank())) {
                        flg = false;
                        break;
                    }
                }
                if (flg) {
                    result.add(flywaySql);
                }
            } else {
                result.add(flywaySql);
            }
        });
        return result;
    }

    private void checkVersion(List<Flyway> dbFlyways, List<FlywaySql> resourceFlyways) {
        if (CollUtil.isEmpty(dbFlyways)) {
            return;
        }
        log.info("开始进行flyway版本校验");
        dbFlyways.forEach(db -> {
            AtomicReference<Integer> resChecksum = new AtomicReference<>(0);
            boolean noMatchVersion = resourceFlyways.stream().anyMatch((r) -> {
                resChecksum.set(r.getChecksum());
                return r.getInstalledRank().equals(db.getInstalledRank()) && (!(r.getChecksum() + "").equals(db.getChecksum() + "") || !r.getVersion().equals(db.getVersion()));
            });
            if (noMatchVersion) {
                throw new FlywayException("flyway版本校验失败, type: {}, version: {}, desc: {}, [本地checksum: {}-数据库checksum: {}]",
                        db.getType(), db.getVersion(), db.getDescription(), resChecksum, db.getChecksum());
            }

            boolean isNoSyncLast = resourceFlyways.stream().noneMatch((r) -> r.getInstalledRank().equals(db.getInstalledRank()));
            if (isNoSyncLast) {
                throw new FlywayException("flyway版本未同步到最新, type: {}, version: {}, desc: {}, installedRank: {}" ,db.getType(), db.getVersion(), db.getDescription(), db.getInstalledRank());
            }
        });

    }

    private void initResourceScript(ResourceScript r, List<Resource> resources) {
        List<FlywaySql> list = new LinkedList<>();

        resources.forEach(resource -> {
            try {
                FlywaySql f = new FlywaySql();
                String sql = this.readFile(resource.getInputStream());
                String fileName = resource.getFilename();
                if (StringUtils.isEmpty(fileName)) {
                    throw new FlywayException("文件名为空");
                }
                String name = fileName.substring(0, fileName.length() - 4);
                f.setDescription(name.split("__")[2]);
                f.setVersion(name.split("__")[1].substring(1));
                f.setSql(sql);
                f.setInstalledBy("admin");
                f.setType(r.getType());
                f.setChecksum(sql.length());
                f.setInstalledOn(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
                f.setScript(fileName);
                String[] version;
                if (f.getVersion().contains(".")) {
                    version = f.getVersion().split("\\.");
                } else {
                    throw new FlywayException("非法版本格式(eg: V1.0.0): {}", f.getVersion());
                }

                int sort = Integer.parseInt(version[0]) * 10000 + Integer.parseInt(version[1]) * 100 + Integer.parseInt(version[2]);
                f.setInstalledRank(sort);
                list.add(f);
            } catch (FlywayException e) {
                throw e;
            } catch (Exception e) {
                log.error("error: ", e);
                throw new FlywayException("初始化flyway脚本失败: " + resource.getFilename());
            }
        });

        r.setSqlList(list.stream().sorted(Comparator.comparing(Flyway::getInstalledRank)).collect(Collectors.toList()));

        for(int i = 1; i <= r.getSqlList().size(); ++i) {
            r.getSqlList().get(i - 1).setInstalledRank(i);
        }
    }

    public List<ResourceScript> loadResource() {
        List<ResourceScript> scriptList = new ArrayList<>();

        Map<String, List<Resource>> resourcesMap = new HashMap<>();
        lyFlywayProperties.getLocations().forEach(location -> {
            try {
                ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
                List<Resource> resources = Arrays.asList(resolver.getResources(location));
                log.info("flyway 加载数据库脚本文件数量: {}",resources.size());
                Map<String, List<Resource>> map = resources.stream().collect(Collectors.groupingBy((rx) -> {
                    // 文件名格式为 type__V版本号__名称
                    String filename = StringUtils.isEmpty(rx.getFilename()) ? "" :rx.getFilename();
                    String[] split = filename.split("__");
                    if (split.length == 0) {
                        String error = String.format("flyway加载数据库脚本文件 %s 失败, 文件名不符合规范, eg: type__V1.0.0__名称.sql / V1.0.0__名称.sql", filename);
                        throw new FlywayException(error);
                    }
                    return split.length == 1 ? "default" : split[0];
                }));
                resourcesMap.putAll(map);
            } catch (IOException e) {
                log.error("加载flyway数据库脚本失败: ", e);
                throw new FlywayException("加载flyway数据库脚本失败-" + e.getMessage());
            }
        });

        resourcesMap.forEach((type, resourcesList) -> {
            ResourceScript rs = new ResourceScript();
            log.info("flyway 加载数据库类型: {}", type);
            rs.setType(type);
            this.initResourceScript(rs, resourcesList);
            scriptList.add(rs);
        });
        return scriptList;
    }
}
